package com.szeldon.declatest.exec;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.logging.Logger;

import com.szeldon.declatest.declaration.TestsSuiteDeclaration;
import com.szeldon.declatest.declaration.endpoints.EndpointDeclaration;
import com.szeldon.declatest.declaration.props.PropertyFileDeclaration;
import com.szeldon.declatest.declaration.reader.exceptions.NotSupportedSettingsException;
import com.szeldon.declatest.declaration.scenario.ScenarioDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.CheckpointPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ConditionPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.DoScenarioStepPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ElsePartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.EndPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ErrorPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.NotComputedPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ScenarioPartDeclaration;
import com.szeldon.declatest.declaration.scenario.parts.ScenarioPartsTypeEnum;
import com.szeldon.declatest.declaration.settings.SettingsDeclaration;
import com.szeldon.declatest.declaration.steps.ScenarioStepDeclaration;
import com.szeldon.declatest.eval.ConditionReturnValue;
import com.szeldon.declatest.eval.exceptions.EvaluationException;
import com.szeldon.declatest.exec.events.CheckpointExecEvent;
import com.szeldon.declatest.exec.events.ElseConditionEvaluationEndExecEvent;
import com.szeldon.declatest.exec.events.ElseConditionEvaluationStartExecEvent;
import com.szeldon.declatest.exec.events.IfConditionEvaluationEndExecEvent;
import com.szeldon.declatest.exec.events.IfConditionEvaluationStartExecEvent;
import com.szeldon.declatest.exec.events.ScenarioExecutionEndExecEvent;
import com.szeldon.declatest.exec.events.ScenarioExecutionStartExecEvent;
import com.szeldon.declatest.exec.events.TestSuiteExecEvent;
import com.szeldon.declatest.exec.events.dostep.DoScenarioStepEndExecEvent;
import com.szeldon.declatest.exec.events.dostep.DoScenarioStepStartExecEvent;
import com.szeldon.declatest.exec.exceptions.TestSuiteException;
import com.szeldon.declatest.exec.plugins.IDoScenarioStepExecutorPlugin;
import com.szeldon.declatest.exec.result.EndScenarioExecutionResult;
import com.szeldon.declatest.exec.result.ErrorScenarioExecutionResult;
import com.szeldon.declatest.exec.result.NotComputedScenarioExecutionResult;
import com.szeldon.declatest.exec.result.ScenarioExecutionResult;
import com.szeldon.declatest.reports.TestSuiteReportData;

/**
 * Executor that does tests suites one by one in single thread and loads all data on start.
 * 
 * @author szeldon
 * 
 */
public class SingleThreadExecutor extends AbstractTestsSuiteExecutor {

    private static Logger logger = Logger.getLogger(SingleThreadExecutor.class.getName());

    @Override
    protected List<TestSuiteReportData> abstractExecute() {

        List<TestSuiteReportData> reportsList = new ArrayList<TestSuiteReportData>();

        for (TestsSuiteDeclaration currentSuite : suitesList) {

            properties = executeTestSuite(currentSuite);

        }

        return reportsList;
    }

    /**
     * Executes given test suite.
     * 
     * @param suite
     * @throws IOException
     * @throws FileNotFoundException
     * @throws TestSuiteException
     */
    private Properties executeTestSuite(TestsSuiteDeclaration suite) throws TestSuiteException {
        logger.info("Starting execution of TestSuite " + suite.getId());

        TestSuiteExecHistory history = new TestSuiteExecHistory(suite.getId());
        historiesList.add(history);
        Properties properties = null;

        try {

            properties = readPropertyFiles(suite.getPropertyFilesList());
            for (ScenarioDeclaration scenario : suite.getScenarios()) {
                logger.info("Starting execution of scenario " + scenario.getId() + ".");

                processEvent(history, new ScenarioExecutionStartExecEvent(scenario.getId(), "Execution starts.",
                        new Date()));

                ScenarioExecutionResult result = executeScenarioParts(suite, properties, scenario, history,
                        scenario.getPartsList());

                processEvent(history, new ScenarioExecutionEndExecEvent(scenario.getId(), "Execution ends.",
                        new Date(), result));

                logger.info("Ending execution of scenario " + scenario.getId() + ".");
            }

        } catch (FileNotFoundException e) {
            logger.severe("File not found. " + e.getMessage());
            throw new TestSuiteException("File not found. " + e.getMessage(), e);
        } catch (IOException e) {
            logger.severe("IO exception. " + e.getMessage());
            throw new TestSuiteException("IO exception. " + e.getMessage(), e);
        } catch (EvaluationException e) {
            logger.severe("Evaluation exception. " + e.getMessage());
            throw new TestSuiteException("Evaluation exception. " + e.getMessage(), e);
        } catch (NotSupportedSettingsException e) {
            logger.severe(e.getMessage());
            throw new TestSuiteException("Execution exception. " + e.getMessage());
        }

        logger.info("Ending execution of TestSuite " + suite.getId());
        return properties;
    }

    /**
     * Adds this event to given history and informs listener about this event.
     * 
     * @param history
     * @param event
     */
    private void processEvent(TestSuiteExecHistory history, TestSuiteExecEvent event) {
        history.addExecEvent(event);
        listener.actionPerformed(event);
    }

    /**
     * Reads properties (returned by this method) from files declared on given list. If there are the same properties in
     * property files, value from the last one will be in returned properties.
     * 
     * @param propertyFiles
     *            List of {@link PropertyFileDeclaration} objects.
     * @return
     * @throws FileNotFoundException
     * @throws IOException
     */
    private Properties readPropertyFiles(List<PropertyFileDeclaration> propertyFiles) throws FileNotFoundException,
            IOException {

        Properties properties = new Properties();

        for (PropertyFileDeclaration currentFile : propertyFiles) {

            properties.load(new FileReader(currentFile.getLocation()));

        }

        return properties;

    }

    /**
     * 
     * @param suite
     * @param properties
     * @param scenario
     * @param history
     * @param partsList
     * @return
     * @throws EvaluationException
     * @throws NotSupportedSettingsException
     */
    private ScenarioExecutionResult executeScenarioParts(TestsSuiteDeclaration suite, Properties properties,
            ScenarioDeclaration scenario, TestSuiteExecHistory history, List<ScenarioPartDeclaration> partsList)
            throws EvaluationException, NotSupportedSettingsException {

        ScenarioExecutionResult result = null;

        boolean isEvaluatedTrue = true;

        for (ScenarioPartDeclaration currentPart : partsList) {

            // don't continue loop if there's already a result for this scenario
            if (result != null) {
                break;
            }

            if (currentPart.isCondition()) {
                BooleanAndScenarioResultContainer returnContainer = executeCondition(
                        (ConditionPartDeclaration) currentPart, isEvaluatedTrue, suite, properties, scenario, history);
                isEvaluatedTrue = returnContainer.isBool();
                result = returnContainer.getResult();
            } // parts not being a condition are evaluated only if condition was evaluated true
            else if (isEvaluatedTrue) {
                ScenarioPartsTypeEnum currentPartType = currentPart.getPartType();
                switch (currentPart.getPartType()) {
                case CHECKPOINT_PART: {
                    executeCheckpoint((CheckpointPartDeclaration) currentPart, suite, properties, scenario, history);
                    break;
                }
                case DO_SCENARIO_STEP_PART: {
                    executeDoScenarioStep((DoScenarioStepPartDeclaration) currentPart, suite, properties, scenario,
                            history);
                    break;
                }
                case RETURN: {
                    executeReturn();
                    break;
                }
                case NOT_COMPUTED: {
                    result = executeNotComputed((NotComputedPartDeclaration) currentPart, suite, properties, scenario,
                            history);
                    break;
                }
                case ERROR_PART: {
                    result = executeError((ErrorPartDeclaration) currentPart, suite, properties, scenario, history);
                    break;
                }
                case END_PART: {
                    result = executeEnd((EndPartDeclaration) currentPart, suite, properties, scenario, history);
                    break;
                }
                default: {
                    throw new IllegalStateException(currentPartType.getName() + " part can't be in that place.");
                }
                }
            }
        }
        return result;

    }

    /**
     * 
     * @param endPart
     * @param suite
     * @param properties
     * @param scenario
     * @param history
     * @return
     */
    private ScenarioExecutionResult executeEnd(EndPartDeclaration endPart, TestsSuiteDeclaration suite,
            Properties properties, ScenarioDeclaration scenario, TestSuiteExecHistory history) {

        EndScenarioExecutionResult result = new EndScenarioExecutionResult(endPart);

        return result;
    }

    /**
     * 
     * @param errorPart
     * @param suite
     * @param properties
     * @param scenario
     * @param history
     * @return
     */
    private ScenarioExecutionResult executeError(ErrorPartDeclaration errorPart, TestsSuiteDeclaration suite,
            Properties properties, ScenarioDeclaration scenario, TestSuiteExecHistory history) {

        ErrorScenarioExecutionResult result = new ErrorScenarioExecutionResult(errorPart);

        return result;

    }

    /**
     * 
     * @param notComputedPart
     * @param suite
     * @param properties
     * @param scenario
     * @param history
     * @return
     */
    private ScenarioExecutionResult executeNotComputed(NotComputedPartDeclaration notComputedPart,
            TestsSuiteDeclaration suite, Properties properties, ScenarioDeclaration scenario,
            TestSuiteExecHistory history) {
        NotComputedScenarioExecutionResult result = new NotComputedScenarioExecutionResult(notComputedPart);

        return result;
    }

    private void executeReturn() {
        // TODO Auto-generated method stub
        throw new UnsupportedOperationException("Not YET implemented.");
    }

    /**
     * 
     * @param doScenarioStepPart
     * @param suite
     * @param properties
     * @param scenario
     * @param history
     * @throws NotSupportedSettingsException
     *             Thrown when there's no executor plugin for a do step.
     */
    private void executeDoScenarioStep(DoScenarioStepPartDeclaration doScenarioStepPart, TestsSuiteDeclaration suite,
            Properties properties, ScenarioDeclaration scenario, TestSuiteExecHistory history)
            throws NotSupportedSettingsException {

        String stepId = doScenarioStepPart.getStepId();
        ScenarioStepDeclaration step = suite.getStepById(stepId);
        String endpointId = step.getEndpointId();
        EndpointDeclaration endpoint = suite.getEndpointById(endpointId);
        String settingsId = endpoint.getSettingsId();
        SettingsDeclaration settings = suite.getSettingsById(settingsId);

        processEvent(
                history,
                new DoScenarioStepStartExecEvent(scenario.getId(), "Starting execution of do scenario step.",
                        new Date(), stepId, step.getDescription(), endpointId, step.getRequestPath(), endpoint
                                .getDescription(), endpoint.getUrl(), endpoint.getSettingsId()));

        // passes execution to plugin supporting endpoint settings
        IDoScenarioStepExecutorPlugin executorPlugin = doScenarioPartExecutors.get(settings.getRequiredServiceName());
        if (executorPlugin == null) {
            throw new NotSupportedSettingsException("There's no plugin to execute a test of \""
                    + settings.getRequiredServiceName() + "\" service.");
        }
        executorPlugin.execute(doScenarioStepPart, step, suite, properties, endpoint, settings, scenario, history,
                listener);

        processEvent(history, new DoScenarioStepEndExecEvent(scenario.getId(), "Ending execution of do scenario step.",
                new Date(), stepId, step.getDescription(), endpointId, step.getRequestPath(),
                endpoint.getDescription(), endpoint.getUrl(), endpoint.getSettingsId()));

    }

    /**
     * 
     * @param checkpointPart
     * @param suite
     * @param properties
     * @param scenario
     * @param history
     */
    private void executeCheckpoint(CheckpointPartDeclaration checkpointPart, TestsSuiteDeclaration suite,
            Properties properties, ScenarioDeclaration scenario, TestSuiteExecHistory history) {

        // TODO is this really all that is needed in checkpoint?

        processEvent(history, new CheckpointExecEvent(scenario.getId(), "Checkpoint executed.", new Date(),
                checkpointPart.getId(), checkpointPart.getDescription()));

    }

    /**
     * 
     * @param currentPart
     * @param isEvaluatedTrue
     * @param suite
     * @param history
     * @param scenario
     * @param properties
     * @throws EvaluationException
     * @throws NotSupportedSettingsException
     */
    private SingleThreadExecutor.BooleanAndScenarioResultContainer executeCondition(
            ConditionPartDeclaration currentPart, boolean isEvaluatedTrue, TestsSuiteDeclaration suite,
            Properties properties, ScenarioDeclaration scenario, TestSuiteExecHistory history)
            throws EvaluationException, NotSupportedSettingsException {

        boolean isIfPart = true;
        if (currentPart instanceof ElsePartDeclaration) {
            isIfPart = false;
        }

        // evaluation happens only for If or for Else but only when If was previously evaluated false
        if (isIfPart || (!isIfPart && !isEvaluatedTrue)) {

            // creates appropriate start event
            TestSuiteExecEvent event = null;
            if (isIfPart) {
                event = new IfConditionEvaluationStartExecEvent(scenario.getId(), "Evaluation starts.", new Date(),
                        currentPart.getLeft(), currentPart.getOperator(), currentPart.getRight(),
                        currentPart.getDescription());
            } else {
                event = new ElseConditionEvaluationStartExecEvent(scenario.getId(), "Evaluation starts.", new Date(),
                        currentPart.getLeft(), currentPart.getOperator(), currentPart.getRight(),
                        currentPart.getDescription());
            }
            processEvent(history, event);

            ConditionReturnValue returnValue = currentPart.evaluate(properties, this, evaluator, history);

            if (isIfPart) {
                event = new IfConditionEvaluationEndExecEvent(scenario.getId(), "Evaluation ends.", new Date(),
                        returnValue.getEvaluatedLeft(), currentPart.getOperator(), returnValue.getEvaluatedRight(),
                        currentPart.getDescription(), returnValue.isEvaluatedTrue());
            } else {
                event = new ElseConditionEvaluationEndExecEvent(scenario.getId(), "Evaluation ends.", new Date(),
                        returnValue.getEvaluatedLeft(), currentPart.getOperator(), returnValue.getEvaluatedRight(),
                        currentPart.getDescription(), returnValue.isEvaluatedTrue());
            }
            processEvent(history, event);

            BooleanAndScenarioResultContainer returnContainer = null;
            if (returnValue.isEvaluatedTrue()) {
                ScenarioExecutionResult result = executeScenarioParts(suite, properties, scenario, history,
                        currentPart.getSubpartsList());
                returnContainer = new BooleanAndScenarioResultContainer(returnValue.isEvaluatedTrue(), result);
            } else {
                returnContainer = new BooleanAndScenarioResultContainer(returnValue.isEvaluatedTrue(), null);
            }

            return returnContainer;
        } else {
            return new BooleanAndScenarioResultContainer(isEvaluatedTrue, null);
        }

    }

    private class BooleanAndScenarioResultContainer {
        private boolean bool;
        private ScenarioExecutionResult result;

        /**
         * 
         * @param bool
         * @param result
         */
        public BooleanAndScenarioResultContainer(boolean bool, ScenarioExecutionResult result) {
            this.bool = bool;
            this.result = result;
        }

        public boolean isBool() {
            return bool;
        }

        public ScenarioExecutionResult getResult() {
            return result;
        }

    }
}
